Week 14 of 16

Build: Test the API

Write tests for your FastAPI endpoints using TestClient — no running server needed.

Day 70 75 minutes Build

Day 70 of 80

Testing HTTP Endpoints

FastAPI provides a TestClient that simulates HTTP requests without actually starting a server. You import your app and make requests against it directly in your test code.

This is fast, reliable, and self-contained. No ports, no network, no timing issues — just your app code responding to test requests.

Create tests/test_api.py

Install the test dependency first:

pip install httpx

Then create tests/test_api.py:

tests/test_api.py Python
# tests/test_api.py — tests for the FastAPI endpoints.
# FastAPI's TestClient simulates HTTP requests without a real server.
# Run with: pytest tests/test_api.py -v
import pytest
from fastapi.testclient import TestClient
from api import app, db

# The TestClient wraps your app and lets you make requests.
# It handles startup and shutdown events automatically.
client = TestClient(app)


# --- GET /prompts ---

def test_list_prompts_returns_list():
    """GET /prompts should return a JSON array."""
    response = client.get("/prompts")
    assert response.status_code == 200
    assert isinstance(response.json(), list)


def test_list_prompts_with_platform_filter():
    """GET /prompts?platform=Kling should only return Kling prompts."""
    # First add a known prompt so we have something to filter on
    data = {"platform": "Kling", "shot": "Filter test shot", "prompt_text": "Filter test prompt"}
    client.post("/prompts", json=data)

    response = client.get("/prompts", params={"platform": "Kling"})
    assert response.status_code == 200
    results = response.json()
    # Every result should be a Kling prompt
    for r in results:
        assert r["platform"] == "Kling"


# --- POST /prompts ---

def test_create_prompt_success():
    """POST /prompts with valid data should return 201 and the created prompt."""
    data = {
        "platform": "Runway",
        "shot": "Test shot from pytest",
        "prompt_text": "Cinematic test prompt for API test"
    }
    response = client.post("/prompts", json=data)

    assert response.status_code == 201
    result = response.json()
    assert result["platform"] == "Runway"
    assert result["shot"] == "Test shot from pytest"
    assert "id" in result  # database assigned an ID
    assert "created_at" in result


def test_create_prompt_invalid_platform():
    """POST /prompts with unrecognized platform should return 422."""
    data = {
        "platform": "Midjourney",  # not a valid platform
        "shot": "test",
        "prompt_text": "test"
    }
    response = client.post("/prompts", json=data)
    # 422 = Unprocessable Entity — Pydantic validation failed
    assert response.status_code == 422


def test_create_prompt_empty_shot():
    """POST /prompts with empty shot should return 422."""
    data = {
        "platform": "Kling",
        "shot": "   ",  # whitespace only
        "prompt_text": "test"
    }
    response = client.post("/prompts", json=data)
    assert response.status_code == 422


# --- GET /prompts/{id} ---

def test_get_prompt_by_id():
    """GET /prompts/{id} should return the prompt if it exists."""
    # Create one first
    data = {"platform": "Veo", "shot": "Get-by-id test", "prompt_text": "Get-by-id prompt"}
    create = client.post("/prompts", json=data)
    prompt_id = create.json()["id"]

    # Now fetch it by ID
    response = client.get(f"/prompts/{prompt_id}")
    assert response.status_code == 200
    assert response.json()["id"] == prompt_id
    assert response.json()["platform"] == "Veo"


def test_get_prompt_not_found():
    """GET /prompts/99999 for a non-existent ID should return 404."""
    response = client.get("/prompts/99999")
    assert response.status_code == 404


# --- DELETE /prompts/{id} ---

def test_delete_prompt():
    """DELETE /prompts/{id} should remove the prompt and return confirmation."""
    # Create one to delete
    data = {"platform": "Kling", "shot": "Delete me", "prompt_text": "To be deleted"}
    create = client.post("/prompts", json=data)
    prompt_id = create.json()["id"]

    # Delete it
    response = client.delete(f"/prompts/{prompt_id}")
    assert response.status_code == 200

    # Verify it's gone
    get_response = client.get(f"/prompts/{prompt_id}")
    assert get_response.status_code == 404


def test_delete_nonexistent_prompt():
    """DELETE /prompts/99999 should return 404."""
    response = client.delete("/prompts/99999")
    assert response.status_code == 404


# --- GET /stats ---

def test_get_stats():
    """GET /stats should return a dictionary mapping platform names to counts."""
    response = client.get("/stats")
    assert response.status_code == 200
    stats = response.json()
    assert isinstance(stats, dict)
    # All values should be integers
    for platform, count in stats.items():
        assert isinstance(count, int)
        assert count > 0

TestClient makes real HTTP-style requests to your app without needing a running server. It's synchronous — you can call it in regular test functions without async def or await.

The create-then-fetch pattern (test_get_prompt_by_id) is the right way to test retrieval. Don't assume any particular data exists in the database — create what you need, then test with it. Tests should be self-contained.

The create-then-delete pattern (test_delete_prompt) verifies the whole lifecycle: create something, delete it, then confirm it's gone. Three HTTP calls, one assertion chain.

422 vs 404: 422 means the request itself was invalid (bad data format). 404 means the request was valid but the resource wasn't found. These are different situations and should return different codes.

Run All Tests

pytest tests/ -v

You'll see the model tests from Days 68–69 and the API tests from today all pass together. That's your full test suite.

Week 14 Complete

You now have a test suite that covers both the data model and the API. Every time you change the code, run pytest and know in seconds if anything broke.

Professional Python projects run tests automatically on every commit. You now understand why — and how.

End of Week Checklist